/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the 'License');
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import ts from '~/vendor/ohos-typescript';

import { NodePredicate } from '~/type';
import { DslAdapter, DslEntry } from '~/dsl';
import { DslRule, EngineCapability } from '~/dsl/Defines';
import { DefineRuleContext, RuleContext } from '~/dsl/Context';
import { AwaitMatch, AwaitReport, AwaitWhen } from '~/dsl/Descriptor';
import { EngineCapabilityHint } from '~/extensions/hint/CapabilityHint';

import { MockRule } from './MockRule';
import { mockEngine } from './MockEngine';
import { MockIssue, MockIssueTemplate } from './MockHint';

const DEFAULT_MOCK_RULE: MockRule = {
    pattern: () => false,
    callback: () => {},
};

type MockRuleBuilderAction<C> = (context: C, rule: MockRule) => void;

/**
 * MockRuleBuilder is a builder for MockRule.
 *
 * Why we need this? Why can't we just build `MockRule` at site?
 * Because the initialization order. Rule might be loaded before the engine
 * and configuration, therefore we have to use callback to build the rule after
 * all the dependencies are ready, or 'on-demand'.
 */
class MockRuleBuilder<C> {
    private actions: MockRuleBuilderAction<C>[] = [];

    public addAction(action: MockRuleBuilderAction<C>) {
        this.actions.push(action);
    }

    /**
     * Some tricks to use closure (is it?) to capture the context and rule.
     */
    public build(): DslRule<MockRule> {
        return (capability: EngineCapability) => {
            const context: RuleContext = {
                props: {},
                ...capability,
            };
            const rule: MockRule = { ...DEFAULT_MOCK_RULE };

            for (const action of this.actions) {
                action(context as C, rule);
            }

            return rule;
        };
    }
}

/**
 * Fluent-style implementation.
 */
class MockAwaitMatch<C extends RuleContext> implements AwaitMatch<C> {
    private readonly builder: MockRuleBuilder<C>;

    constructor(builder: MockRuleBuilder<C>) {
        this.builder = builder;
    }

    match<T extends ts.Node>(param: NodePredicate<T> | ((node: ts.Node, context: C) => node is T)): AwaitWhen<T, C> {
        if (typeof param === 'function') {
            this.matchWithAction<T>(param);
        } else {
            this.matchWithPredicate<T>(param);
        }
        return new MockAwaitThenAndReport(this.builder);
    }

    private matchWithPredicate<T extends ts.Node>(predicate: NodePredicate<T>): void {
        this.builder.addAction((_, rule: MockRule) => {
            rule.pattern = predicate.is;
        });
    }

    private matchWithAction<T extends ts.Node>(predicate: (node: ts.Node, context: C) => node is T): void {
        this.builder.addAction((context: C, rule: MockRule) => {
            rule.pattern = (node: ts.Node) => predicate(node, context);
        });
    }
}

/**
 * Fluent-style implementation.
 */
class MockAwaitThenAndReport<T extends ts.Node, C extends RuleContext> implements AwaitWhen<T, C>, AwaitReport<T, C> {
    private builder: MockRuleBuilder<C>;

    // Default `when` behavior is to always return true.
    private whenAction: (node: T, context: C) => boolean = () => true;

    constructor(builder: MockRuleBuilder<C>) {
        this.builder = builder;
    }

    when(action: (node: T, context: C) => boolean): AwaitReport<T, C> {
        this.whenAction = action;
        return this;
    }

    report(param: (node: T, context: C) => void | MockIssueTemplate): DslRule<MockRule> {
        if (typeof param === 'function') {
            this.reportImplAction(param);
        } else {
            this.reportImplTemplate(param);
        }
        return this.builder.build();
    }

    private reportImplAction(action: (node: T, context: C) => void): void {
        this.builder.addAction((context, rule: MockRule) => {
            rule.callback = (node: ts.Node) => {
                if (this.whenAction(node as T, context)) {
                    action(node as T, context);
                }
            };
        });
    }

    private reportImplTemplate(template: MockIssueTemplate): void {
        this.builder.addAction((context, rule: MockRule) => {
            rule.callback = (node: ts.Node) => {
                const issue: MockIssue = {
                    line: context.api.getLocation(node).range.start.line,
                    message: template.message.replace(/\{line}/g, node.pos.toString()),
                };
                if (this.whenAction(node as T, context)) {
                    context.api.report(issue);
                }
            };
        });
    }
}

function entry<PropsType, HintType extends EngineCapabilityHint, U extends DefineRuleContext<PropsType, HintType>>(
    init: (context: RuleContext<HintType>) => PropsType
): AwaitMatch<U> {
    const builder = new MockRuleBuilder<U>();
    builder.addAction((context: U, _: MockRule) => {
        context.props = init(context);
    });
    return new MockAwaitMatch<U>(builder);
}

/**
 * An example of custom DSL adapter.
 */
export class MockAdapter extends DslAdapter<MockRule> {
    /**
     * This is necessary, as the `super()` constructor is required to automatically
     * register this adapter.
     */
    public constructor() {
        console.log('Hello');
        super();
    }

    entry: DslEntry<any, any, any> = entry;

    loadDslRule(dslRule: DslRule<MockRule>): MockRule {
        return dslRule(mockEngine.getCapabilities());
    }
}

/**
 * Only one instance of the adapter should be created.
 */
export const mockAdapter = new MockAdapter();
